Hallitse toimialuelähtöistä suunnittelua JavaScriptissä. Opi moduulientiteettimalli rakentaaksesi skaalautuvia, testattavia ja ylläpidettäviä sovelluksia vahvoilla toimialueobjektimalleilla.
JavaScript-moduulientiteettimallit: Syväsukellus toimialueobjektien mallintamiseen
Ohjelmistokehityksen maailmassa, erityisesti dynaamisessa ja jatkuvasti kehittyvässä JavaScript-ekosysteemissä, priorisoimme usein nopeutta, kehyksiä ja ominaisuuksia. Rakennamme monimutkaisia käyttöliittymiä, yhdistämme lukemattomiin API-rajapintoihin ja otamme sovelluksia käyttöön huimaa vauhtia. Mutta tässä kiireessä laiminlyömme joskus sovelluksemme ytimen: liiketoiminta-alueen. Tämä voi johtaa siihen, mitä usein kutsutaan "suureksi mutapalloksi" – järjestelmäksi, jossa liiketoimintalogiikka on hajallaan, data on jäsentymätöntä ja yksinkertaisen muutoksen tekeminen voi laukaista odottamattomien virheiden vyöryn.
Tässä kohtaa toimialueobjektien mallintaminen tulee kuvaan. Se on käytäntö luoda rikas ja ilmaisuvoimainen malli ongelma-alueesta, jossa työskentelet. Ja JavaScriptissä Moduulientiteettimalli on tehokas, elegantti ja kehyksestä riippumaton tapa saavuttaa tämä. Tämä kattava opas vie sinut läpi tämän mallin teorian, käytännön ja hyödyt, mikä antaa sinulle mahdollisuuden rakentaa vankempia, skaalautuvampia ja ylläpidettävämpiä sovelluksia.
Mikä on toimialueobjektien mallintaminen?
Ennen kuin sukellamme itse malliin, selvennetään termejämme. On erittäin tärkeää erottaa tämä käsite selaimen Document Object Model (DOM) -mallista.
- Toimialue: Ohjelmistossa "toimialue" on se erityinen aihealue, johon käyttäjän liiketoiminta kuuluu. Verkkokauppasovelluksessa toimialue sisältää käsitteitä, kuten Tuotteet, Asiakkaat, Tilaukset ja Maksut. Sosiaalisen median alustassa se sisältää Käyttäjät, Julkaisut, Kommentit ja Tykkäykset.
- Toimialueobjektien mallintaminen: Tämä on prosessi, jossa luodaan ohjelmistomalli, joka edustaa entiteettejä, niiden käyttäytymistä ja niiden suhteita kyseisellä liiketoiminta-alueella. Kyse on todellisten käsitteiden kääntämisestä koodiksi.
Hyvä toimialuemalli ei ole vain kokoelma datakontteja. Se on liiketoimintasääntöjesi elävä esitys. Tilaus-objektin ei pitäisi vain sisältää luetteloa tuotteista; sen pitäisi osata laskea kokonaissumma, lisätä uusi tuote ja tietää, voidaanko se peruuttaa. Tämä datan ja käyttäytymisen kapselointi on avain kestävän sovelluksen ytimen rakentamiseen.
Yleinen ongelma: Anarkia "malli"-kerroksessa
Monissa JavaScript-sovelluksissa, erityisesti orgaanisesti kasvavissa, "malli"-kerros on usein jälkihuomio. Näemme usein tämän vastamallin:
// Jossain API-ohjaimessa tai palvelussa...
async function createUser(req, res) {
const { email, password, firstName, lastName } = req.body;
// Liiketoimintalogiikka ja validointi on hajallaan täällä
if (!email || !email.includes('@')) {
return res.status(400).send({ error: 'Kelvollinen sähköpostiosoite vaaditaan.' });
}
if (!password || password.length < 8) {
return res.status(400).send({ error: 'Salasanan on oltava vähintään 8 merkkiä pitkä.' });
}
const user = {
email: email.toLowerCase(),
password: await hashPassword(password), // Jokin apufunktio
fullName: `${firstName} ${lastName}`, // Logiikka johdetulle datalle on täällä
createdAt: new Date()
};
// Mikä on `user`? Se on vain tavallinen objekti.
// Mikään ei estä toista kehittäjää tekemästä tätä myöhemmin:
// user.email = 'an-invalid-email';
// user.password = 'short';
await db.users.insert(user);
res.status(201).send(user);
}
Tämä lähestymistapa tuo mukanaan useita kriittisiä ongelmia:
- Ei yhtä totuuden lähdettä: Säännöt, jotka määrittävät kelvollisen "käyttäjän", on määritetty tämän yhden ohjaimen sisällä. Entä jos jokin muu järjestelmän osa tarvitsee käyttäjän luomista? Kopioitko-liitätkö logiikan? Tämä johtaa epäjohdonmukaisuuteen ja virheisiin.
- Aneminen toimialuemalli: `user`-objekti on vain "tyhmä" datalaukku. Sillä ei ole käyttäytymistä eikä itsetietoisuutta. Kaikki logiikka, joka toimii sen kanssa, elää ulkoisesti.
- Matala koheesio: Logiikka käyttäjän koko nimen luomiseen on sekoitettu API-pyyntöjen/vastausten käsittelyyn ja salasanan hajautukseen.
- Vaikea testata: Käyttäjän luontilogiikan testaamiseksi sinun on mallinnettava HTTP-pyynnöt ja -vastaukset, tietokannat ja hajautusfunktiot. Et voi vain testata "käyttäjä"-käsitettä erikseen.
- Implisiittiset sopimukset: Muun sovelluksen on vain "olettettava", että mikä tahansa käyttäjää edustava objekti on tietyn muotoinen ja että sen tiedot ovat kelvollisia. Ei ole takeita.
Ratkaisu: JavaScript-moduulientiteettimalli
Moduulientiteettimalli ratkaisee nämä ongelmat käyttämällä tavallista JavaScript-moduulia (yksi tiedosto) määrittämään kaiken yhdestä toimialuekonseptista. Tästä moduulista tulee kyseisen entiteetin lopullinen totuuden lähde.
Moduulientiteetti paljastaa tyypillisesti tehdasfunktion. Tämä funktio on vastuussa entiteetin kelvollisen instanssin luomisesta. Sen palauttama objekti ei ole vain dataa; se on rikas toimialueobjekti, joka kapseloi oman datansa, validoinnin ja liiketoimintalogiikan.
Moduulientiteetin keskeiset ominaisuudet
- Kapselointi: Se niputtaa datan ja funktiot, jotka toimivat kyseisellä datalla, yhteen.
- Validointi rajalla: Se varmistaa, että on mahdotonta luoda virheellistä entiteettiä. Se vartioi omaa tilaansa.
- Selkeä API: Se paljastaa puhtaan ja tarkoituksellisen funktiojoukon (julkisen API:n) vuorovaikutusta varten entiteetin kanssa piilottaen samalla sisäiset toteutustiedot.
- Muuttumattomuus: Se tuottaa usein muuttumattomia tai vain luku -objekteja estämään tahattomat tilan muutokset ja varmistamaan ennustettavan käyttäytymisen.
- Siirrettävyys: Sillä ei ole riippuvuuksia kehyksiin (kuten Express, React) tai ulkoisiin järjestelmiin (kuten tietokantoihin, API-rajapintoihin). Se on puhdasta liiketoimintalogiikkaa.
Moduulientiteetin ydin komponentit
Rakennetaan uudelleen `User`-konseptimme käyttämällä tätä mallia. Luomme tiedoston `user.js` (tai `user.ts` TypeScript-käyttäjille) ja rakennamme sen vaihe vaiheelta.
1. Tehdasfunktio: Objektin rakentaja
Luokkien sijaan käytämme tehdasfunktiota (esim. `buildUser`). Tehtaat tarjoavat suurta joustavuutta, välttävät painimisen `this`-avainsanan kanssa ja tekevät yksityisestä tilasta ja kapseloinnista luonnollisempaa JavaScriptissä.
Tavoitteenamme on luoda funktio, joka ottaa raakadataa ja palauttaa hyvin muodostetun ja luotettavan User-objektin.
// tiedosto: /domain/user.js
export default function buildMakeUser() {
// Tämä sisäinen funktio on varsinainen tehdas.
// Sillä on pääsy kaikkiin riippuvuuksiin, jotka on välitetty buildMakeUser-funktiolle, jos tarpeen.
return function makeUser({
id = generateId(), // Oletetaan funktio yksilöllisen ID:n luomiseksi
firstName,
lastName,
email,
passwordHash,
createdAt = new Date()
}) {
// ... validointi ja logiikka menevät tähän ...
const user = {
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => email,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt
};
// Käytetään Object.freeze-funktiota, jotta objekti olisi muuttumaton.
return Object.freeze(user);
}
}
Huomaa muutama asia tässä. Käytämme funktiota, joka palauttaa funktion (korkeamman asteen funktion). Tämä on tehokas malli riippuvuuksien injektoimiseksi, kuten yksilöllisen ID:n luoja tai validerointikirjasto, ilman, että entiteetti kytketään tiettyyn toteutukseen. Pidetään se nyt yksinkertaisena.
2. Datan validointi: Vartija portilla
Entiteetin on suojeltava omaa eheyttään. Pitää olla mahdotonta luoda `User` virheellisessä tilassa. Lisäämme validoinnin suoraan tehdasfunktion sisään. Jos data on virheellistä, tehtaan pitäisi heittää virhe, jossa selvästi kerrotaan, mikä on vialla.
// tiedosto: /domain/user.js
export default function buildMakeUser({ Id, isValidEmail, hashPassword }) {
return function makeUser({
id = Id.makeId(),
firstName,
lastName,
email,
password, // Otamme nyt tavallisen salasanan ja käsittelemme sen sisällä
createdAt = new Date()
}) {
if (!Id.isValidId(id)) {
throw new Error('Käyttäjällä on oltava kelvollinen tunnus.');
}
if (!firstName || firstName.length < 2) {
throw new Error('Etunimen on oltava vähintään 2 merkkiä pitkä.');
}
if (!lastName || lastName.length < 2) {
throw new Error('Sukunimen on oltava vähintään 2 merkkiä pitkä.');
}
if (!email || !isValidEmail(email)) {
throw new Error('Käyttäjällä on oltava kelvollinen sähköpostiosoite.');
}
if (!password || password.length < 8) {
throw new Error('Salasanan on oltava vähintään 8 merkkiä pitkä.');
}
// Datan normalisointi ja muuntaminen tapahtuu tässä
const passwordHash = hashPassword(password);
const normalizedEmail = email.toLowerCase();
return Object.freeze({
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => normalizedEmail,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt
});
}
}
Nyt minkä tahansa järjestelmämme osan, joka haluaa luoda `User`-olion, on mentävä tämän tehtaan läpi. Saamme taatun validoinnin joka kerta. Olemme myös kapseloineet salasanan hajautuksen ja sähköpostiosoitteen normalisoinnin logiikan. Muun sovelluksen ei tarvitse tietää tai välittää näistä yksityiskohdista.
3. Liiketoimintalogiikka: Käyttäytymisen kapselointi
`User`-objektimme on edelleen hieman aneeminen. Se sisältää dataa, mutta se ei *tee* mitään. Lisätään käyttäytymistä – metodeja, jotka edustavat toimialuekohtaisia toimintoja.
// ... makeUser-funktion sisällä ...
if (!password || password.length < 8) {
// ...
}
const passwordHash = hashPassword(password);
const normalizedEmail = email.toLowerCase();
return Object.freeze({
getId: () => id,
getFirstName: () => firstName,
getLastName: () => lastName,
getEmail: () => normalizedEmail,
getPasswordHash: () => passwordHash,
getCreatedAt: () => createdAt,
// Liiketoimintalogiikka / Käyttäytyminen
getFullName: () => `${firstName} ${lastName}`,
// Metodi, joka kuvaa liiketoimintasääntöä
canVote: () => {
// Joissakin maissa äänestysikä on 18 vuotta. Tämä on liiketoimintasääntö.
// Oletetaan, että meillä on dateOfBirth-ominaisuus.
const age = calculateAge(dateOfBirth);
return age >= 18;
}
});
// ...
`getFullName`-logiikka ei ole enää hajallaan jossain satunnaisessa ohjaimessa; se kuuluu `User`-entiteetille itselleen. Kuka tahansa, jolla on `User`-objekti, voi nyt luotettavasti saada koko nimen kutsumalla `user.getFullName()`. Logiikka on määritetty kerran, yhdessä paikassa.
Käytännöllisen esimerkin rakentaminen: Yksinkertainen verkkokauppajärjestelmä
Käytetään tätä mallia enemmän yhteenliittyvään toimialueeseen. Mallinnamme `Product`-olion, `OrderItem`-olion ja `Order`-olion.
1. `Product`-entiteetin mallintaminen
Tuotteella on nimi, hinta ja varastotiedot. Sillä on oltava nimi, eikä sen hinta voi olla negatiivinen.
// tiedosto: /domain/product.js
export default function buildMakeProduct({ Id }) {
return function makeProduct({
id = Id.makeId(),
name,
description,
price,
stock = 0
}) {
if (!Id.isValidId(id)) {
throw new Error('Tuotteella on oltava kelvollinen tunnus.');
}
if (!name || name.trim().length < 2) {
throw new Error('Tuotteen nimen on oltava vähintään 2 merkkiä pitkä.');
}
if (isNaN(price) || price <= 0) {
throw new Error('Tuotteen hinnan on oltava suurempi kuin nolla.');
}
if (isNaN(stock) || stock < 0) {
throw new Error('Varaston on oltava ei-negatiivinen luku.');
}
return Object.freeze({
getId: () => id,
getName: () => name,
getDescription: () => description,
getPrice: () => price,
getStock: () => stock,
// Liiketoimintalogiikka
isAvailable: () => stock > 0,
// Metodi, joka muokkaa tilaa palauttamalla uuden instanssin
reduceStock: (amount) => {
if (amount > stock) {
throw new Error('Saatavilla ei ole tarpeeksi varastoa.');
}
// Palauta UUSI tuoteobjekti päivitetyllä varastolla
return makeProduct({ id, name, description, price, stock: stock - amount });
}
});
}
}
Huomaa `reduceStock`-metodi. Tämä on olennainen konsepti, joka liittyy muuttumattomuuteen. Sen sijaan, että muutettaisiin `stock`-ominaisuutta olemassa olevassa objektissa, se palauttaa *uuden* `Product`-instanssin päivitetyllä arvolla. Tämä tekee tilan muutoksista eksplisiittisiä ja ennustettavia.
2. `Order`-entiteetin mallintaminen (Aggregaattijuuri)
`Order` on monimutkaisempi. Se on se, mitä toimialuelähtöinen suunnittelu (DDD) kutsuu "Aggregaattijuuriksi". Se on entiteetti, joka hallinnoi muita, pienempiä objekteja rajojensa sisällä. `Order` sisältää luettelon `OrderItem`-olioita. Et lisää tuotetta suoraan tilaukseen; lisäät `OrderItem`-olion, joka sisältää tuotteen ja määrän.
// tiedosto: /domain/order.js
export const ORDER_STATUS = {
PENDING: 'PENDING',
PAID: 'PAID',
SHIPPED: 'SHIPPED',
CANCELLED: 'CANCELLED'
};
export default function buildMakeOrder({ Id, validateOrderItem }) {
return function makeOrder({
id = Id.makeId(),
customerId,
items = [],
status = ORDER_STATUS.PENDING,
createdAt = new Date()
}) {
if (!Id.isValidId(id)) {
throw new Error('Tilauksella on oltava kelvollinen tunnus.');
}
if (!customerId) {
throw new Error('Tilauksella on oltava asiakastunnus.');
}
let orderItems = [...items]; // Luo yksityinen kopio hallintaa varten
return Object.freeze({
getId: () => id,
getCustomerId: () => customerId,
getItems: () => [...orderItems], // Palauta kopio estämään ulkoista muokkausta
getStatus: () => status,
getCreatedAt: () => createdAt,
// Liiketoimintalogiikka
calculateTotal: () => {
return orderItems.reduce((total, item) => {
return total + (item.getPrice() * item.getQuantity());
}, 0);
},
addItem: (item) => {
// validateOrderItem on funktio, joka varmistaa, että tuote on kelvollinen OrderItem-entiteetti
validateOrderItem(item);
// Liiketoimintasääntö: estä kaksoiskappaleiden lisääminen, lisää vain määrää
const existingItemIndex = orderItems.findIndex(i => i.getProductId() === item.getProductId());
if (existingItemIndex > -1) {
const newQuantity = orderItems[existingItemIndex].getQuantity() + item.getQuantity();
// Tässä päivität määrän olemassa olevassa tuotteessa
// (Tämä edellyttää, että tuotteet ovat muokattavissa tai niillä on päivitysmetodi)
} else {
orderItems.push(item);
}
},
markPaid: () => {
if (status !== ORDER_STATUS.PENDING) {
throw new Error('Vain odottavat tilaukset voidaan merkitä maksetuiksi.');
}
// Palauta uusi Order-instanssi päivitetyllä tilalla
return makeOrder({ id, customerId, items: orderItems, status: ORDER_STATUS.PAID, createdAt });
}
});
}
}
- Se hallinnoi omaa tuoteluetteloaan.
- Se osaa laskea oman kokonaissummansa.
- Se noudattaa tilan siirtymiä (esim. voit merkitä vain `PENDING`-tilauksen `PAID`-tilaan).
Tilausten liiketoimintalogiikka on nyt siististi kapseloitu tähän moduuliin, testattavissa erikseen ja uudelleenkäytettävissä koko sovelluksessasi.
Kehittyneet mallit ja huomioitavat asiat
Muuttumattomuus: Ennustettavuuden kulmakivi
Olemme jo koskeneet muuttumattomuutta. Miksi se on niin tärkeää? Kun objektit ovat muuttumattomia, voit siirtää niitä sovelluksessasi pelkäämättä, että jokin etäinen funktio muuttaa niiden tilaa odottamatta. Tämä poistaa kokonaisen virheluokan ja tekee sovelluksesi datavirrasta paljon helpompaa päätellä.
Object.freeze() tarjoaa matalan jäädytyksen. Entiteeteille, joissa on sisäkkäisiä objekteja tai taulukoita (kuten `Order`-oliosi), sinun on oltava varovaisempi. Esimerkiksi funktiossa `order.getItems()` palautimme kopion (`[...orderItems]`) estääksemme soittajaa työntämästä tuotteita suoraan tilauksen sisäiseen taulukkoon.
Monimutkaisissa sovelluksissa kirjastot, kuten Immer, voivat helpottaa huomattavasti työskentelyä muuttumattomien sisäkkäisten rakenteiden kanssa, mutta ydinperiaate säilyy: käsittele entiteettejäsi muuttumattomina arvoina. Kun muutos on tehtävä, luo uusi arvo.
Asynkronisten toimintojen ja pysyvyyden käsittely
Olet ehkä huomannut, että entiteettimme ovat täysin synkronisia. Ne eivät tiedä mitään tietokannoista tai API-rajapinnoista. Tämä on tarkoituksellista ja mallin suuri vahvuus!
Entiteettien ei pitäisi tallentaa itseään. Entiteetin tehtävänä on noudattaa liiketoimintasääntöjä. Datan tallentaminen tietokantaan kuuluu sovelluksesi toiselle kerrokselle, jota kutsutaan usein nimellä Palvelukerros, Käyttötapauskerros tai Repository-malli.
Tässä on, miten ne ovat vuorovaikutuksessa:
// tiedosto: /use-cases/create-user.js
// Tämä käyttötapaus on riippuvainen käyttäjäentiteettitehtaasta ja tietokannan käyttöfunktiosta.
export default function makeCreateUser({ makeUser, usersDatabase }) {
return async function createUser(userInfo) {
// 1. Luo kelvollinen toimialue-entiteetti. Tämä vaihe validoi datan.
const user = makeUser(userInfo);
// 2. Tarkista liiketoimintasäännöt, jotka vaativat ulkoista dataa (esim. sähköpostiosoitteen yksilöllisyys)
const exists = await usersDatabase.findByEmail({ email: user.getEmail() });
if (exists) {
throw new Error('Sähköpostiosoite on jo käytössä.');
}
// 3. Säilytä entiteetti. Tietokanta tarvitsee tavallisen objektin.
const persisted = await usersDatabase.insert({
id: user.getId(),
firstName: user.getFirstName(),
// ... ja niin edelleen
});
return persisted;
}
}
- `User`-entiteetti on puhdas, synkroninen ja helppo yksikkötestata.
- `createUser`-käyttötapaus on vastuussa orkestroinnista ja se voidaan integroida testatuksi valetietokannan kanssa.
- `usersDatabase`-moduuli on vastuussa tietystä tietokantatekniikasta ja se voidaan testata erikseen.
Sarjallistaminen ja deserialisointi
Entiteettisi, metodeineen, ovat rikkaita objekteja. Mutta kun lähetät dataa verkon yli (esim. JSON API -vastauksessa) tai tallennat sen tietokantaan, tarvitset tavallisen datan esityksen. Tätä prosessia kutsutaan sarjallistamiseksi.
Yleinen malli on lisätä `toJSON()`- tai `toObject()`-metodi entiteettiisi.
// ... makeUser-funktion sisällä ...
return Object.freeze({
getId: () => id,
// ... muut getterit
// Sarjallistamismenetelmä
toObject: () => ({
id,
firstName,
lastName,
email: normalizedEmail,
createdAt
// Huomaa, että emme sisällytä passwordHashia
})
});
Käänteinen prosessi, jossa otetaan tavallista dataa tietokannasta tai API-rajapinnasta ja muutetaan se takaisin rikkaaksi toimialueentiteetiksi, on juuri sitä, mitä `makeUser`-tehdasfunktiosi tekee. Tämä on deserialisointi.
Kirjoittaminen TypeScriptillä tai JSDocilla
Vaikka tämä malli toimii täydellisesti tavallisessa JavaScriptissä, staattisten tyyppien lisääminen TypeScriptillä tai JSDocilla tehostaa sitä huomattavasti. Tyyppien avulla voit muodollisesti määrittää entiteettisi "muodon", mikä tarjoaa erinomaisen automaattisen täydennyksen ja käännösaikaiset tarkistukset.
// tiedosto: /domain/user.ts
// Määritä entiteetin julkinen rajapinta
export type User = Readonly<{
getId: () => string;
getFirstName: () => string;
// ... jne
getFullName: () => string;
}>;
// Tehdasfunktio palauttaa nyt User-tyypin
export default function buildMakeUser(...) {
return function makeUser(...): User {
// ... toteutus
}
}
Moduulientiteettimallin kattavat edut
Ottamalla tämän mallin käyttöön saat lukuisia etuja, jotka kasaantuvat sovelluksesi kasvaessa:
- Yksi totuuden lähde: Liiketoimintasäännöt ja datan validointi ovat keskitettyjä ja yksiselitteisiä. Säännön muutos tehdään täsmälleen yhdessä paikassa.
- Korkea koheesio, matala kytkeytyneisyys: Entiteetit ovat itsenäisiä eivätkä ole riippuvaisia ulkopuolisista järjestelmistä. Tämä tekee koodipohjastasi modulaarisen ja helposti uudelleenjärjesteltävän.
- Ylivoimainen testattavuus: Voit kirjoittaa yksinkertaisia, nopeita yksikkötestejä kriittisimmälle liiketoimintalogiikallesi ilman, että sinun tarvitsee mallintaa koko maailmaa.
- Parannettu kehittäjäkokemus: Kun kehittäjän on työskenneltävä `User`-olion kanssa, hänellä on selkeä, ennustettava ja itse dokumentoiva API käytettävissä. Ei enää arvailua tavallisten objektien muodon suhteen.
- Perusta skaalautuvuudelle: Tämä malli antaa sinulle vakaan ja luotettavan ytimen. Kun lisäät lisää ominaisuuksia, kehyksiä tai UI-komponentteja, liiketoimintalogiikkasi pysyy suojattuna ja johdonmukaisena.
Johtopäätös: Rakenna vankka ydin sovelluksellesi
Nopeasti liikkuvien kehysten ja kirjastojen maailmassa on helppo unohtaa, että nämä työkalut ovat ohimeneviä. Ne muuttuvat. Mikä kestää, on liiketoiminta-alueesi ydinlogiikka. Ajan sijoittaminen tämän toimialueen asianmukaiseen mallintamiseen ei ole vain akateeminen harjoitus; se on yksi merkittävimmistä pitkäaikaisista investoinneista, joita voit tehdä ohjelmistosi terveyteen ja pitkäikäisyyteen.
JavaScript-moduulientiteettimalli tarjoaa yksinkertaisen, tehokkaan ja natiivin tavan toteuttaa näitä ideoita. Se ei vaadi raskasta kehystä tai monimutkaista asennusta. Se hyödyntää kielen perusominaisuuksia – moduuleja, funktioita ja sulkeumia – auttaakseen sinua rakentamaan puhtaan, kestävän ja ymmärrettävän ytimen sovelluksellesi. Aloita yhdellä avainentiteetillä seuraavassa projektissasi. Mallinna sen ominaisuudet, validoi sen luominen ja anna sille käyttäytymistä. Otat ensimmäisen askeleen kohti vankempaa ja ammattimaisempaa ohjelmistoarkkitehtuuria.